/**
 * Written by Yuval Oren for Attributor. Contributed to the public domain in April, 2010. 
 * Provides Subversion integration with FitNesse using the CM_SYSTEM mechanism available in FitNesse version 20100303
 */

package com.attributor.fitnesse;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNCommitClient;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNStatusClient;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.core.wc.SVNWCClient;
import org.tmatesoft.svn.core.wc.SVNWCUtil;

public class SubversionCmSystem {
	private static List<String> ignoredPaths = new ArrayList<String>();
	private static Map<String, SVNClientManager> clientManagerCache = new HashMap<String, SVNClientManager>();
	private static Pattern payloadPattern = Pattern
			.compile(".+?\\s+(.*?)/(.*?)\\s*"); // class svnuser/svnpassword
	private static String hostname;
	private static String workingDir;

	static {
		ignoredPaths.add("/RecentChanges/");
		ignoredPaths.add("/ErrorLogs/");

		try {
			hostname = InetAddress.getLocalHost().getHostName();
		} catch (UnknownHostException e) {
			hostname = "(unknown host)";
		}

		try {
			workingDir = new File(".").getCanonicalPath();
		} catch (IOException e) {
			workingDir = "(unknown directory)";
		}

		// SVN setup
		DAVRepositoryFactory.setup();
		SVNRepositoryFactoryImpl.setup();
		FSRepositoryFactory.setup();
	}

	/**
	 * Called before saving the changed file
	 * 
	 * @param file
	 *            path to file
	 * @param payload
	 *            contents of CM_SYSTEM
	 * @throws IOException
	 * @throws SVNException
	 */
	public static void cmEdit(String file, String payload) throws IOException,
			SVNException {
		if (isIgnored(file)) {
			return;
		}

		File f = new File(file);
		SVNClientManager svn = getClientManager(payload);

		
		SVNStatusType contentStatus = svnStatus(svn,f);

		if (contentStatus == SVNStatusType.STATUS_UNVERSIONED) {
			svnAdd(svn, f);
		} else if (contentStatus == SVNStatusType.STATUS_ADDED
				|| contentStatus == SVNStatusType.STATUS_MODIFIED
				|| contentStatus == SVNStatusType.STATUS_IGNORED
				|| contentStatus == SVNStatusType.STATUS_NORMAL
				|| contentStatus == SVNStatusType.STATUS_NONE) {

			if (svnUpdate(svn, f)) {
				throw new IllegalStateException(
						"The page was out of date, as there was a newer revision in subversion. Please try again.");
			}
		} else {
			throw new UnsupportedOperationException(
					"Don't yet know how to deal with subversion status "
							+ contentStatus);
		}
	}

	/**
	 * Called after the file is saved
	 * 
	 * @param file
	 *            path to file
	 * @param payload
	 *            contents of CM_SYSTEM
	 * @throws IOException
	 * @throws SVNException
	 */
	public static void cmUpdate(String file, String payload)
			throws IOException, SVNException {
		if (isIgnored(file)) {
			return;
		}

		svnCommit(getClientManager(payload), new File(file));
	}

	/**
	 * Called before the file is deleted
	 * 
	 * @param file
	 *            path to file
	 * @param payload
	 *            contents of CM_SYSTEM
	 * @throws IOException
	 * @throws SVNException
	 */
	public static void cmPreDelete(String file, String payload)
			throws IOException, SVNException {
		if (isIgnored(file)) {
			return;
		}

		SVNWCClient wcc = getClientManager(payload).getWCClient();
		wcc.doDelete(new File(file), true, false);
	}

	/**
	 * Called after the file is deleted
	 * 
	 * @param file
	 *            path to file
	 * @param payload
	 *            contents of CM_SYSTEM
	 * @throws IOException
	 * @throws SVNException
	 */
	public static void cmDelete(String file, String payload)
			throws IOException, SVNException {
		if (isIgnored(file)) {
			return;
		}

		svnCommit(getClientManager(payload), new File(file));
	}
	
	
	// *** Subversion commands ***

	public static SVNStatusType svnStatus(SVNClientManager svn, File f) throws SVNException {
		SVNStatusType contentStatus;
		try {
			contentStatus = svn.getStatusClient().doStatus(f, true)
					.getContentsStatus();
		} catch (SVNException e) {
			// Hack to prevent exceptions for files not yet checked in. There
			// must be a standard way to do this in SVNKit
			if (e.getMessage().contains("is not a working copy")) {
				contentStatus = SVNStatusType.STATUS_UNVERSIONED;
			} else {
				throw e;
			}
		}
		return contentStatus;
		
	}
	
	private static void svnAdd(SVNClientManager svn, File f)
			throws SVNException {
		
		SVNWCClient wcc = svn.getWCClient();
		wcc.doAdd(f, true, false, true, SVNDepth.EMPTY, false, true);
	}


	/**
	 * Refreshes a file to the latest committed version
	 * 
	 * @param svn
	 * @param f
	 * @return true if the file changed, false if it was already current
	 * @throws SVNException
	 */
	private static boolean svnUpdate(SVNClientManager svn, File f)
			throws SVNException {
		SVNStatusClient sc = svn.getStatusClient();
		long oldRev = sc.doStatus(f, false).getCommittedRevision().getNumber();
		svn.getUpdateClient().doUpdate(f, SVNRevision.HEAD, SVNDepth.INFINITY,
				true, true);
		long newRev = sc.doStatus(f, false).getCommittedRevision().getNumber();

		return oldRev != newRev;
	}

	private static void svnCommit(SVNClientManager svn, File f)
			throws IOException, SVNException {

		// If it's a file, include its parent directory
		File[] files;
		if (f.isFile()) {
			files = new File[]{f, f.getParentFile()};
		}
		else {
			files = new File[]{f};
		}
		
		SVNCommitClient cc = svn.getCommitClient();
		cc.doCommit(files, false,
				"FitNesse checkin from " + hostname + ":" + workingDir, null,
				null, false, false, SVNDepth.EMPTY);
	}

	private static boolean isIgnored(String filePath) {
		File currentFile = new File(filePath);
		String absolutePath = currentFile.getAbsolutePath();

		for (String ignoredItem : ignoredPaths) {
			if (absolutePath.contains(ignoredItem))
				return true;
		}

		return false;
	}

	private static synchronized SVNClientManager getClientManager(String payload)
			throws IOException {
		SVNClientManager mgr = clientManagerCache.get(payload);
		if (mgr != null) {
			return mgr;
		}

		Matcher payloadMatcher = payloadPattern.matcher(payload);

		if (!payloadMatcher.matches()) {
			throw usage();
		}

		String user = payloadMatcher.group(1);
		String password = payloadMatcher.group(2);

		mgr = SVNClientManager.newInstance(SVNWCUtil
				.createDefaultOptions(false), user, password);
		clientManagerCache.put(payload, mgr);
		return mgr;
	}

	private static IllegalArgumentException usage() {
		return new IllegalArgumentException("CM_SYSTEM should be: "
				+ SubversionCmSystem.class.getName() + " user/password");
	}

}
